package cs340.stormtroopers.hypeerweb;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.TreeSet;

import phase6.GlobalObjectId;
import phase6.LocalObjectId;
import phase6.ObjectDB;

/**
 * Represents a Node in a HyPeerWeb. <br/>
 * <br/>
 * 
 * <pre>
 * <b>Instance Domain:</b>
 * 		connections : Connections
 * 		webId       : WebId
 * 		state       : NodeState
 * <b>Instance Invariants:</b>
 * 		webId is a non-null valid WebId (id >= -1)
 * 		state is always non-null and correctly represents the node's actual state
 * 			- if node is end cap node, state is EndCapNode
 * 			- otherwise, if node has down pointers, state is DownPointingNode
 * 			- otherwise, state is NormalNode
 * 		connections is a non-null valid Connections object valid for the Node's webId
 * 			- neighbors differ by one bit in their webIds
 * 			- the fold's webId is the complement of the Node's webId
 * 			- etc. - see invariants for the Connections class
 * 		contents is a non-null Contents object
 * <b>Class Domain and Invariants:</b>
 * 		None
 * </pre>
 * 
 * @author Andy Morrow
 * @author Brad Spendlove
 * @author Daniel Howard
 * @author James Gurr
 * @author William Highland
 * 
 */

public class Node implements Serializable, Comparable
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 5749286839848869617L;

	/**
	 * Singleton to implement the null object pattern. One and only null Node.
	 */
	public static final NullNode NULL_NODE = new NullNode();

	private LocalObjectId localId;

	/**
	 * The node's current WebId.
	 */
	private WebId webId;
	/**
	 * The node's list of connections.
	 */
	private Connections connections;
	/**
	 * The node's current state.
	 */
	private NodeState state;
	/**
	 * The Node's contents.
	 */
	private Contents contents;

	/////////////////////////////////////////////////////////// CONSTRUCTORS
	protected Node()
	{
		// used by NullNode and NodeProxy classes
		invalid(NULL_NODE);
	}

	/**
	 * Sets local variables correctly for a null node.
	 * 
	 * @pre None.
	 * @post The node is configured as a null node. IE<br>
	 *       webId = -1 AND<br>
	 *       |contents| = 0 AND<br>
	 *       |connections| = 0 AND<br>
	 *       state = NormalNode
	 */
	public void invalid(NullNode nullNode)
	{
		int newWebId = -1;
		int height = -1;
		this.webId = new WebId(newWebId, height);

		connections = new Connections(nullNode, nullNode, nullNode);

		state = Invalid.getSingleton();
		contents = new Contents();
		localId = new LocalObjectId(phase6.GlobalLocalObjectIds.INVALID_NODE);
		//ObjectDB.getSingleton().store(localId, this);
	}

	/**
	 * Public constructor creates a node with the given webId.
	 * 
	 * @param webId
	 *            the new WebId's integer value
	 * @pre webId >= 0
	 * @post webId = webId with the integer given (see WebId constraints) AND<br>
	 *       node.fold = node AND<br>
	 *       |neighbors| = |surrogateNeighbors| = |downPointers| = |upPointers|
	 *       = 0 AND<br>
	 *       |contents| = 0 AND<br>
	 *       node.state meets instance invariants - see above
	 */
	public Node(int webId)
	{
		assert (webId >= 0);

		this.webId = new WebId(webId);
		TreeSet<Node> neighbors = new TreeSet<Node>();
		ArrayList<Node> upPointers = new ArrayList<Node>();
		ArrayList<Node> downPointers = new ArrayList<Node>();
		Node fold = this;
		Node surrogateFold = NULL_NODE;
		Node inverseSurrogateFold = NULL_NODE;
		connections = new Connections(neighbors, upPointers, downPointers,
				fold, surrogateFold, inverseSurrogateFold);

		updateState();
		contents = new Contents();
		localId = new LocalObjectId();
		ObjectDB.getSingleton().store(localId, this);
	}

	/**
	 * Public constructor creates a new node with the given WebId and height.
	 * 
	 * @param webId
	 *            the integer value of the new WebId
	 * @param height
	 *            the number of significant bits or height of the WebId
	 * @pre webId >= 0 AND<br>
	 *      height <= 31 Note that in this implementation we use 32 bit
	 *      integers; thus the restriction on height. AND<br>
	 *      height >= the index of the highest significant bit in webId
	 * @post webId = webId with the value and height given (see WebId
	 *       constraints) AND<br>
	 *       node.fold = node AND<br>
	 *       |neighbors| = |surrogateNeighbors| = |downPointers| = |upPointers|
	 *       = 0 AND<br>
	 *       |contents| = 0 AND<br>
	 *       node.state meets instance invariants - see above
	 */
	public Node(int webId, int height)
	{
		assert (webId >= 0);
		assert (height <= 31);
		assert (height >= WebId.locationOfMostSignificantOneBit(webId) + 1);
		this.webId = new WebId(webId, height);
		TreeSet<Node> neighbors = new TreeSet<Node>();
		ArrayList<Node> upPointers = new ArrayList<Node>();
		ArrayList<Node> downPointers = new ArrayList<Node>();
		Node fold = this;
		Node surrogateFold = NULL_NODE;
		Node inverseSurrogateFold = NULL_NODE;
		connections = new Connections(neighbors, upPointers, downPointers,
				fold, surrogateFold, inverseSurrogateFold);

		updateState();
		contents = new Contents();
		localId = new LocalObjectId();
		ObjectDB.getSingleton().store(localId, this);
	}

	/**
	 * Copy constructor.
	 * 
	 * @param other
	 *            a valid Node to copy
	 * @pre other != NULL_NODE and is a valid node (see instance invariants)
	 * @post the new Node is an exact and complete deep copy of other IE<br>
	 *       node.webId = other.webId<br>
	 *       node.connections = other.connections<br>
	 *       node.contents = other.contents<br>
	 *       node.state = other.state
	 */
	public Node(Node other)
	{
		this.webId = new WebId(other.getWebIdValue(), other.webId.getHeight());
		//this.state = other.state; // not needed - we update state below
		// Copy all node connections to this node
		connections = new Connections(other.connections);
		contents = new Contents(other.contents);

		updateState();
		localId = new LocalObjectId();
		ObjectDB.getSingleton().store(localId, this);
	}

	/**
	 * Used with the Java serialization library to send Proxies across the network, not Nodes.
	 * @pre None
	 * @post result = a NodeProxy whose GlobalObjectId is the GlobalObjectId of this Node
	 * @throws ObjectStreamException
	 */
	protected Object writeReplace() throws ObjectStreamException
	{
		if (HyPeerWeb.getSingleton().getNodesShouldSerializeToProxies())
			return new NodeProxy(new GlobalObjectId(localId));
		else
			return this;
	}

	/**
	 * Returns a string representation of this node.
	 * 
	 * @pre None
	 * @post Returns a string representation of this node.
	 */
	public String toString()
	{
		return "WebId: " + webId.getValue() + " State: " + state.toString()
				+ " Height: " + webId.getHeight() + " "
				+ connections.toString() + " LocalObjectId: " + getLocalObjectId();
	}

	/**
	 * Compares the passed node to this node
	 * 
	 * @return value > 0 when passed node's webId is smaller value = 0 when the
	 *         nodes' webId is the same value < 0 when the passed node's webId
	 *         is larger
	 * 
	 * @pre Both nodes have instantiated webIds
	 * @post See return
	 */
	public int compareTo(Object node)
	{
		return getWebIdValue() - ((Node) node).getWebIdValue();
	}

	public boolean equals(Object other)
	{
		Node otherNode = (Node) other;
		return this.getWebIdValue() == otherNode.getWebIdValue();
	}

	// //////////////////////////////////////////////////////////// PUBLIC
	// GETTERS / SETTERS

	/**
	 * Gets the Node's localObjectId
	 */
	public LocalObjectId getLocalObjectId()
	{
		return localId;
	}
	
	public boolean setLocalObjectId(LocalObjectId local)
	{
		localId = local;
		return true;
	}

	/**
	 * Gets the contents of the Node.
	 * 
	 * @pre None
	 * @post returns the contents
	 */
	public Contents getContents()
	{
		return contents;
	}

	/**
	 * Gets the list of down-pointers (surrogate neighbors).
	 * 
	 * @post result = list of down-pointers (surrogate neighbors)
	 */
	public ArrayList<Node> getDownPointers()
	{
		return connections.getDownPointers();
	}

	/**
	 * Adds a down-pointer to the list.
	 * 
	 * @param downPointer
	 *            a node to point to
	 * @pre the passed down pointer is not already in this Node's list of down
	 *      pointers
	 * @post |downPointers|' = |downPointers| + 1 AND<br>
	 *       the new downPointer is the last item in the downPointer list AND<br>
	 *       node.state meets the instance invariants
	 */
	public boolean addDownPointer(Node downPointer)
	{
		connections.addDownPointer(downPointer);
		updateState();
		return true;
	}

	/**
	 * Removes a down-pointer.
	 * 
	 * @param downPointer
	 *            a node to remove from the down-pointers list
	 * @pre downPointer != null AND<br>
	 *      downPointer is a member of this Node's list of down-pointers
	 * @post |downPointers|' = |downPointers| - 1 AND<br>
	 *       the given downPointer is no longer in downPointers AND<br>
	 *       node.state meets instance invariants
	 */
	public boolean removeDownPointer(Node downPointer)
	{
		connections.removeDownPointer(downPointer);
		updateState();
		return true;
	}

	/**
	 * Adds a neighbor.
	 * 
	 * @param neighbor
	 *            a Node to add as a neighbor
	 * @pre neighbor is not already in the Node's neighbor list AND<br>
	 *      neighbor != null AND<BR>
	 *      neighbor.webId is a neighbor to this Node's webId (see WebId
	 *      constraints)
	 * @post |neighbors|' = |neighbors| + 1 AND<br>
	 *       neighbor is a member of neighbors
	 */
	public boolean addNeighbor(Node neighbor)
	{
		// assumption: this is only called from within a valid web
		// TODO make sure this works
		connections.addNeighbor(neighbor);
		return true;
	}

	/**
	 * Returns a complete list of this node's neighbors.
	 * 
	 * @pre None
	 * @post returns a complete list of this node's neighbors.
	 */
	public ArrayList<Node> getNeighbors()
	{
		ArrayList<Node> neighbors = new ArrayList<Node>(
				connections.getNeighbors());

		return neighbors;
	}

	public ArrayList<Node> getNeighborsBiggerThanMe()
	{
		ArrayList<Node> neighborsBiggerThanMe = new ArrayList<Node>(
				connections.getNeighborsBiggerThanMe(this));

		return neighborsBiggerThanMe;
	}

	/**
	 * Sets all the neighbors of this node.
	 * 
	 * @param parentsSmaller
	 *            a list of neighbor nodes who have children and have webids
	 *            smaller than this node
	 * @param childlessSmaller
	 *            a list of neighbor nodes who do not have children and have
	 *            webids smaller than this node
	 * @param bigger
	 *            a list of neighbor nodes whose webids are bigger than this
	 *            node
	 * @pre The three lists of valid lists of neighbors.
	 * @post The Node's neighbors are updated with deep copies of these lists.
	 *       IE<br>
	 *       node.parentNeighborsSmallerThanMe = parentsSmaller<br>
	 *       node.childlessNeighborsSmallerThanMe = childlessSmaller<br>
	 *       node.neighborsBiggerThanMe = bigger
	 */
	// private void setNeighbors(ArrayList<Node> parentsSmaller,
	// ArrayList<Node> childlessSmaller, ArrayList<Node> bigger)
	// {
	// connections.setParentNeighborsSmallerThanMe(parentsSmaller);
	// connections.setChildlessNeighborsSmallerThanMe(childlessSmaller);
	// connections.setNeighborsBiggerThanMe(bigger);
	// }

	/**
	 * Adds an up-pointer.
	 * 
	 * @param upPointer
	 *            a node to point to
	 * @pre upPointer is a valid up-pointer for this node as defined by its
	 *      webid (see WebId constraints) AND<br>
	 *      upPointer is not a member of node.upPointers
	 * @post |upPointers|' = |upPointers| + 1 AND<br>
	 *       upPointer is a member of upPointers AND<br>
	 *       upPointer is the last item in the list of upPointers
	 */
	public boolean addUpPointer(Node upPointer)
	{
		connections.addUpPointer(upPointer);
		return true;
	}

	/**
	 * Returns all the up-pointers
	 * 
	 * @pre None
	 * @post returns a list of all the nodes this node points up to
	 */
	public ArrayList<Node> getUpPointers()
	{
		return connections.getUpPointers();
	}

	/**
	 * Creates a simplified node domain to be used in testing.
	 * 
	 * @pre this node is valid = no null connections or null lists - only
	 *      NULL_NODE and empty lists allowed (see instance invariants)
	 * @post result = a new SimplifiedNodeDomain AND<br>
	 *       result.webid = (int)node.webid AND<br>
	 *       result.neighbors = (int)node.neighbors AND<br>
	 *       result.upPointers = (int)node.upPointers AND<br>
	 *       result.downPointers = (int)node.downPointers AND<br>
	 *       result.fold = (int)node.fold AND<br>
	 *       result.surrogateFold = (int)node.surrogateFold AND<br>
	 *       result.inverseSurrogateFold = (int)node.inverseSurrogateFold
	 */
	public SimplifiedNodeDomain constructSimplifiedNodeDomain()
	{
		HashSet<Integer> neighborInts = new HashSet<Integer>();
		for (Node n : getNeighbors())
			neighborInts.add(n.getWebIdValue());

		HashSet<Integer> upPointerInts = new HashSet<Integer>();
		for (Node n : getUpPointers())
			upPointerInts.add(n.getWebIdValue());

		HashSet<Integer> downPointerInts = new HashSet<Integer>();
		for (Node n : getDownPointers())
			downPointerInts.add(n.getWebIdValue());

		int foldInt, surrogateFoldInt, inverseSurrogateFoldInt;

		Node fold = getFold();
		foldInt = fold.getWebIdValue();

		Node surrogateFold = getSurrogateFold();
		surrogateFoldInt = surrogateFold.getWebIdValue();

		Node inverseSurrogateFold = getInverseSurrogateFold();
		inverseSurrogateFoldInt = inverseSurrogateFold.getWebIdValue();

		return new SimplifiedNodeDomain(webId.getValue(), webId.getHeight(),
				neighborInts, upPointerInts, downPointerInts, foldInt,
				surrogateFoldInt, inverseSurrogateFoldInt, state.getStateId());
	}

	/**
	 * Gets the connections object associated with thisNode
	 * 
	 * @post result = the complete Connections object for this node
	 */
	public Connections getConnections()
	{
		return connections;
	}

	/**
	 * Gets the connections its child node will have upon insertion into the
	 * HyPeerWeb.
	 * 
	 * @post result = a new Connections object where:<br>
	 *       result.downPointers = node.neighbors such that the neighbors webId's are bigger than this node's webId AND<br>
	 *       result.neighbors = node.upPointers AND<br>
	 *       result.fold meets the WebId instance invariants for the child's
	 *       WebId (also determined by WebId instances invariants)
	 */
	public Connections getChildConnections()
	{
		return connections.getChildConnections(this);
	}

	/**
	 * Returns the WebId of the Node.
	 * 
	 * @pre None
	 * @post returns the WebId
	 */
	public WebId getWebId()
	{
		return webId;
	}

	/**
	 * Returns the integer value of the Node's WebId.
	 * 
	 * @pre None
	 * @post returns the WebId as an integer
	 */
	public int getWebIdValue()
	{
		return webId.getValue();
	}

	/**
	 * Removes a neighbor from this Node's list.
	 * 
	 * @param neighbor
	 *            Node to be removed
	 * @pre The neighbor parameter is a Node which is already in this Node's
	 *      list of neighbors
	 * @post The node is no longer in this Node's list of neighbors IE<br>
	 *       |node.neighbors|' = |node.neighbors| - 1 AND<br>
	 *       the other neighbors in the list are unaffected.
	 */
	public boolean removeNeighbor(Node neighbor)
	{
		// TODO check assumptions
		// assumption: only used from within a valid web
		connections.removeNeighbor(neighbor);
		return true;
	}

	/**
	 * Removes an up-pointer (inverse surrogate neighbor) from this Node's list.
	 * 
	 * @param upPointer
	 *            the Node to be removed
	 * @pre the Node to be removed is an inverse surrogate neighbor of this Node
	 * @post the Node to be removed is no longer in this Node's list of
	 *       up-pointers IE<br>
	 *       |node.upPointers|' = |node.upPointers| - 1 AND<br>
	 *       the other up-pointers remain unaffected.
	 */
	public boolean removeUpPointer(Node upPointer)
	{
		connections.removeUpPointer(upPointer);
		return true;
	}

	/**
	 * Resets all of the connections for this Node.
	 * 
	 * @param connections
	 *            the new set of connections
	 * @pre the passed Connections are valid and non-null (see Connections
	 *      instance invariants)
	 * @post this Node now has the given connections AND<br>
	 *       the Nodes in the Connections object passed are not informed of this
	 *       change
	 */
	public boolean setConnections(Connections connections)
	{
		this.connections = new Connections(connections);
		this.updateState();
		return true;
	}

	/**
	 * Sets the fold of this Node
	 * 
	 * @param newFold
	 *            Node to point to
	 * @pre newFold is not NULL AND<br>
	 *      is in the same HyPeerWeb AND<br>
	 *      newFold meets the WebId constraints for this Node (see WebId
	 *      constraints)
	 * @post node.fold = newFold
	 */
	public boolean setFold(Node newFold)
	{
		// setting surrogateFold to NULL_NODE is done by somebody else in
		// updateInsertionPoint
		connections.setFold(newFold);
		return true;
	}

	/**
	 * Returns this Node's fold
	 * 
	 * @pre None
	 * @post returns the fold or the NULL_NODE
	 */
	public Node getFold()
	{
		return connections.getFold();
	}

	/**
	 * Sets this Node's inverse surrogate fold.
	 * 
	 * @param newInverseSurrogateFold
	 *            the Node to point to
	 * @pre the new Node is not null AND<br>
	 *      resides in the same HyPeerWeb
	 * @post this Node now points to the passed Node as its inverse surrogate
	 *       fold
	 */
	public boolean setInverseSurrogateFold(Node newInverseSurrogateFold)
	{
		connections.setInverseSurrogateFold(newInverseSurrogateFold);
		return true;
	}

	/**
	 * Returns this Node's inverse surrogate fold.
	 * 
	 * @pre None
	 * @post returns the inverse surrogate fold or the NULL_NODE
	 */
	public Node getInverseSurrogateFold()
	{
		return connections.getInverseSurrogateFold();
	}

	/**
	 * Sets this Node's surrogate fold.
	 * 
	 * @param newSurrogateFold
	 *            the Node to point to
	 * @pre the Node to point to is non-null AND<br>
	 *      resides in the same HyPeerWeb
	 * @post this Node now points to the passed Node as its surrogate fold
	 */
	public boolean setSurrogateFold(Node newSurrogateFold)
	{
		connections.setSurrogateFold(newSurrogateFold);
		return true;
	}

	/**
	 * Returns this Node's surrogate fold.
	 * 
	 * @pre None
	 * @post returns the surrogate fold or the NULL_NODE
	 */
	public Node getSurrogateFold()
	{
		return connections.getSurrogateFold();
	}

	/**
	 * Sets this node to have a new WebId.
	 * 
	 * @param webId
	 *            the new WebId
	 * @pre the new WebId is non-null AND<br>
	 *      the new WebId is consistent (valid height for value etc.) (see
	 *      constraints) AND<br>
	 *      the new WebId is valid in the current web (see constraints)
	 * @post this Node has the new WebId AND<br>
	 *       has updated its internal state accordingly (instance invariants
	 *       satisfied)
	 */
	public boolean setWebId(WebId webId)
	{
		this.webId = webId;
		updateState();
		return true;
	}

	/**
	 * Prints the value of the fold or surrogate fold.<br>
	 * Mainly for debugging.
	 * 
	 * @pre this Node's fold is non-null OR<br>
	 *      this Node's surrogate fold is non-null
	 * @post the correct WebId has been printed to the terminal
	 */
	public void printFold()
	{
		if (connections.getFold() != null)
		{
			Node fold = connections.getFold();
			System.out.println("Node " + webId.getValue() + "'s fold is "
					+ fold.getWebIdValue());
		}
		else
		{
			Node surrogateFold = connections.getSurrogateFold();
			System.out.println("Node " + webId.getValue()
					+ "'s surrogate fold is " + surrogateFold.getWebIdValue());
		}
	}

	/**
	 * Node's implementation of the Visitor pattern.
	 * 
	 * @pre visitor must not be null.
	 * @post The node will have been visited according to the implementation of
	 *       visitor.
	 * 
	 * @param visitor
	 *            The Visitor object that will visit this Node.
	 * @param parameters
	 *            A Parameters object that fits the constraints of visitor
	 */
	public boolean accept(Visitor visitor, Parameters parameters)
	{
		visitor.visit(this, parameters);
		return true;
	}

	// /////////////////////////////////////////////////////// ADDING NODES

	/**
	 * Adds a node to the HyPeerWeb that this Node belongs to.
	 * 
	 * @param newNode
	 *            the Node to add to the HyPeerWeb
	 * @pre This Node is a member of a HyPeerWeb AND<br>
	 *      the other Node is not already a member of the same HyPeerWeb AND<br>
	 *      the other Node is in a valid, consistent state
	 * @post The other Node is now a member of the HyPeerWeb IE<br>
	 *       |hyPeerWeb|' = |hyPeerWeb| + 1 AND<br>
	 *       it has been inserted as the highest Node in the web (IE it has the
	 *       greatest WebId) AND<br>
	 *       its neighbors (as determined by its WebId) have been notified of
	 *       its presence AND<br>
	 *       its surrogate neighbors, fold, and other direct connections have
	 *       been notified of its presence AND<br>
	 *       parent.height' = parent.height + 1 AND<br>
	 *       the parent's connections have been updated to meet design
	 *       constraints
	 * 
	 */
	public boolean addToHyPeerWeb(Node newNode)
	{
		if (newNode == null)
			newNode = new Node(42);

		//		System.out.println(webId.getValue() + ".AddToHyPeerWeb() with parameter node: " + newNode.getWebIdValue());

		Node insertionPoint = this.findEndCapNode().findInsertionPoint();
		newNode.setWebId(insertionPoint.getWebId().createChildsWebId());

		newNode.setConnections(insertionPoint.getChildConnections());

		newNode.getFold().setFold(newNode);
		//		System.out.println("This BEFORE updateAssociates: \n" + this.toString());
		//		System.out.println("NewNodes Fold BEFORE updateAssociates: \n" + newNode.getFold().toString());
		newNode.updateNewNodesAssociates();
		//		System.out.println("This AFTER updateAssociates: \n" + this.toString());
		//		System.out.println("NewNodes Fold AFTER updateAssociates: \n" + newNode.getFold().toString());

		insertionPoint.updateInsertionPoint();
		insertionPoint.addNeighbor(newNode);
		insertionPoint.updateState();
		newNode.updateState();
		//		System.out.println("This AFTER all of addToHyPeerWeb: \n" + this.toString());

		newNode.addMeToMyHyPeerWeb();
		return true;
	}

	public boolean addMeToMyHyPeerWeb()
	{
		HyPeerWeb.getSingleton().addNode(this);
		return true;
	}

	public boolean updateInsertionPoint()
	{

		int myWebId = getWebIdValue();
		int foldsWebId = getFold().getWebIdValue();
		int foldsFoldWebId = getFold().getFold().getWebIdValue();

		//		System.out.println("My Web id is " + myWebId);
		//		System.out.println("My Fold's Web Id is " + foldsWebId);
		//		System.out.println("My Fold's Fold's Web Id is " + foldsFoldWebId);

		//		System.out.println("Insertion point at BEGINNING of UpdateInsertionPoint:\n" + this.toString());

		// notes: parent's new surrFold is its old fold
		if (myWebId == 0 && foldsWebId == 1
				&& foldsFoldWebId == myWebId)
		{
			System.out.println("NO OP in Update insertion point");
		}
		else if (getInverseSurrogateFold() == NULL_NODE)
		{
			setSurrogateFold(getFold());
			setFold(NULL_NODE);
			getSurrogateFold().setInverseSurrogateFold(this);
			System.out.println(getFold());
		}
		else
		{
			getInverseSurrogateFold().setSurrogateFold(NULL_NODE);
			setInverseSurrogateFold(NULL_NODE);
		}

		// No longer an surrogate neighbor, so removing self from up pointer's
		// down pointers
		//		System.out.println("Insertion point " + getWebIdValue() + " is removing downPointers");
		for (Node inverseSurrogateNeighbor : connections.getUpPointers())
			inverseSurrogateNeighbor.removeDownPointer(this);
		connections.clearUpPointers();

		webId.setHeight(webId.getHeight() + 1);

		// for (Node neighbor : connections.getNeighborsBiggerThanMe())
		// neighbor.neighborHadChild(this);
		return true;
	}

	public boolean updateNewNodesAssociates()
	{
		for (Node neighbor : connections
				.getChildlessNeighborsSmallerThanMe(this))
			neighbor.addNeighbor(this);

		for (Node surrogateNeighbor : connections.getDownPointers())
			surrogateNeighbor.addUpPointer(this);

		return true;

		//		System.out.println("Inside updateNewNodesAssociates my connections are: \n" + connections.toString());
		//		System.out.println("My fold IN THE MIDDLE of updateNewNodesAssociates is: \n" + connections.getFold().toString());


		//		connections.getFold().setFold(this);
		//		System.out.println("My fold AT THE END of updateNewNodesAssociates is: \n" + connections.getFold().toString());

	}

	// assumptions: neighbor is a real neighbor of this node
	// private void neighborHadChild(Node neighbor)
	// {
	// connections.neighborHadChild(neighbor);
	// }

	public Node findSmallestChildlessNeighbor()
	{
		Node smallest = this;

		/*
		 * for (Node Current : getNeighbors()) if (current.getWebIdValue() <
		 * smallest.getWebIdValue() && current.getWebId().getHeight() ==
		 * webId.getHeight()) smallest = current;
		 */

		for (Node current : connections.getParentNeighborsSmallerThanMe(this))
			if (current.getWebIdValue() < smallest.getWebIdValue()
					&& current.getWebId().getHeight() == webId.getHeight())
				smallest = current;

		for (Node current : connections
				.getChildlessNeighborsSmallerThanMe(this))
			if (current.getWebIdValue() < smallest.getWebIdValue()
					&& current.getWebId().getHeight() == webId.getHeight())
				smallest = current;

		if (smallest.getWebIdValue() != getWebIdValue())
			return smallest.findSmallestChildlessNeighbor();
		else
			return this;

		/*
		 * return !childlessNeighborsSmallerThanMe.isEmpty() ?
		 * childlessNeighborsSmallerThanMe
		 * .get(0).findSmallestChildlessNeighbor() : this;
		 */
	}

	public Node findEndCapNode()
	{
		return fromMeToEndCapNode();
	}

	public Node findInsertionPoint()
	{
		return fromMeToInsertionPoint();
	}

	public Node fromMeToInsertionPoint()
	{
		return state.findInsertionPoint(this);
	}

	public Node fromMeToEndCapNode()
	{
		return state.findEndCapNode(this);
	}

	////////////////////////////////////////////////////////////// REMOVING
	// NODES

	/**
	 * Removes this Node from the HyPeerWeb it is currently a member of.
	 * 
	 * @pre This node belongs to a valid HyPeerWeb AND<br>
	 *      is correctly connected to its neighbors etc. AND<br>
	 *      it is not the only node in the HyPeerWeb
	 * 
	 * @post This Node is no longer in the HyPeerWeb AND<br>
	 *       if this Node was NOT the deletion point, the deletion point has
	 *       been moved into its place AND<br>
	 *       its former neighbors, parents, fold, etc. have had their
	 *       connections updated to point to the Node which took its place OR<br>
	 *       in this case this Node was the deletion point, they have been
	 *       updated with surrogate neighbors and surrogate folds etc. according
	 *       to HyPeerWeb constraints AND<br>
	 *       its parent's height has been reduced by one AND<br>
	 *       there is exactly one Node less in the HyPeerWeb AND<br>
	 *       all other nodes in the HyPeerWeb remain unaffected
	 */
	public boolean removeFromHyPeerWeb()
	{
		try
		{
			Node deletionPoint = null;
			// Corner case when there is only one node left in the web
			if (getNeighbors().size() == 0)
			{
				System.out.println("You can't delete the last node in the entire HyPeerWeb! ");
				setFold(this);
				setSurrogateFold(NULL_NODE);
				setInverseSurrogateFold(NULL_NODE);
			}
			else
			{
				deletionPoint = findDeletionPoint();
				assert (deletionPoint != NULL_NODE);

				//		HyPeerWeb.getSingleton().printToAllTracePanels("The deletion point is: " + deletionPoint.toString());

				deletionPoint.removeSelf();
				if (deletionPoint.getWebIdValue() != this.getWebIdValue())
				{
					HyPeerWeb.getSingleton().removeNode(this);
					try
					{
						deletionPoint.takePlaceOf(this);
					}
					catch(Exception e)
					{
						e.printStackTrace();
						System.err.println("EXCEPTION THROWN: " + e.getMessage());
					}
					// we'll hold off on this optimization until we know it all works
					// right
					// connections.clearAll();
				}
			}

		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		return true;
	}

	public boolean removeSelf()
	{

		Node parent = findParent(); // WARNING! Look out for Null-Pointer
		assert (parent != null);

		new PointDownToParent(this).execute(parent);
		new RemoveSelfFromNeighbors(this).execute(this);
		new RemoveSelfAsUpPointer(this).execute(this);

		// FOLD STUFFS
		Node fold = getFold();
		fold.disconnectMeAsFold(parent);
		parent.disconnectMeAsChild(fold);

		/*
		 * OLD BAD FOLD STUFFS
		 * 
		 * if (parent.getFold() == NULL_NODE) { getFold().setFold(parent);
		 * parent.setFold(getFold());
		 * parent.getInverseSurrogateFold().setInverseSurrogateFold(NULL_NODE);
		 * parent.setSurrogateFold(NULL_NODE);
		 * parent.getFold().setInverseSurrogateFold(NULL_NODE); } else {
		 * getFold().setFold(NULL_NODE); getFold().setSurrogateFold(parent);
		 * parent.setInverseSurrogateFold(getFold());
		 * parent.getFold().setInverseSurrogateFold(NULL_NODE); }
		 */

		// We should only need to update parent's height
		parent.setHeight(parent.getHeight() - 1);
		parent.updateState();

		//Be sure to remove from appropriate Hypeerweb segment
		HyPeerWeb.getSingleton().removeNode(this);

		return true;
	}

	public int getHeight()
	{
		return webId.getHeight();
	}

	public boolean setHeight(int newHeight)
	{
		webId.setHeight(newHeight);

		return true;
	}

	public boolean disconnectMeAsChild(Node foldOfLeavingNode)
	{
		if (getFold().getWebIdValue() != -1)
		{
			// don't change fold
			setInverseSurrogateFold(foldOfLeavingNode);
		}
		else
		{
			setSurrogateFold(NULL_NODE);
			setFold(foldOfLeavingNode);
			// essentially just swapping surrfold <-> fold
		}
		return true;

	}

	public boolean disconnectMeAsFold(Node parentOfLeavingNode)
	{
		if (getInverseSurrogateFold().getWebIdValue() != -1)
		{
			setFold(getInverseSurrogateFold());
			setInverseSurrogateFold(NULL_NODE);
		}
		else
		{
			setSurrogateFold(parentOfLeavingNode);
			setFold(NULL_NODE);
		}
		return true;
	}

	public Node findParent()
	{
		// Quick and dirty solution
		/*
		 * Potential time saver: return parentNeighborsSmallerThanMe.get(0);
		 */
		int parentId = webId.getParentsValue();

		for (Node neighbor : getNeighbors())
			if (neighbor.getWebIdValue() == parentId)
				return neighbor;
		return NULL_NODE;
	}

	public boolean takePlaceOf(Node leavingNode)
	{
		// so that nothing ends up as its own neighbor
		leavingNode.removeNeighbor(this);

		// set all connections
		webId = leavingNode.getWebId();
		setConnections(leavingNode.getConnections());
		updateState();

		HyPeerWeb.getSingleton().addNode(this);
		return true;
	}

	// Andy's alg:
	// Find end cap node, take it's fold. follow to largest neighbor

	/**
	 * Finds the node with the highest ID in the entire HyPeerWeb - aka the
	 * deletion point.
	 * 
	 * @pre This Node is a member of a valid HyPeerWeb.
	 * @post returns the deletion point Node
	 */
	public Node findDeletionPoint()
	{
		Node start = findEndCapNode();
		if (start.getFold().getWebIdValue() == 0)
			return start;
		start = start.getFold();
		return start.followBiggestNeighbor();
	}

	public Node followBiggestNeighbor()
	{
		Node biggest = this;
		for (Node n : getNeighbors())
			if (n.getWebIdValue() > biggest.getWebIdValue())
				biggest = n;
		if (biggest.getWebIdValue() == webId.getValue())
			return biggest;
		else
			return biggest.followBiggestNeighbor();
	}

	// ////////////////////////////////////////////////////////////////////
	// STATES

	public boolean updateState()
	{
		if (Math.pow(2, webId.getHeight()) - 1 == webId.getValue())
			state = EndCapNode.getSingleton();
		else if (!getDownPointers().isEmpty())
			state = DownPointingNode.getSingleton();
		else
			state = NormalNode.getSingleton();

		return true;
	}

	public static class Invalid extends NodeState
	{
		private static final long serialVersionUID = 576403879894452532L;
		public static int STATE_ID = -1;

		public int getStateId()
		{
			return STATE_ID;
		}

		private static Invalid singleton = null;

		private Invalid()
		{
		}

		public Node findInsertionPoint(Node node)
		{
			throw new IllegalStateException();
		}

		public Node findEndCapNode(Node node)
		{
			throw new IllegalStateException();
		}

		public static Invalid getSingleton()
		{
			if (singleton == null)
				singleton = new Invalid();
			return singleton;
		}

		public String toString()
		{
			return "INVALID";
		}

	}

	public static class NormalNode extends NodeState
	{
		private static final long serialVersionUID = 1484511319597575925L;
		public static int STATE_ID = 0;

		public int getStateId()
		{
			return STATE_ID;
		}

		private static NormalNode singleton = null;

		private NormalNode()
		{
		}

		public Node findInsertionPoint(Node node)
		{
			return node.findSmallestChildlessNeighbor();
		}

		public Node findEndCapNode(Node node)
		{
			Node largestNeighbor = null;
			int largestWebId = -42;

			for (Node n : node.getConnections().getNeighbors())
			{
				if (n.getWebIdValue() > largestWebId)
				{
					largestWebId = n.getWebIdValue();
					largestNeighbor = n;
				}
			}
			return largestNeighbor.findEndCapNode();
		}

		public static NormalNode getSingleton()
		{
			if (singleton == null)
				singleton = new NormalNode();
			return singleton;
		}

		public String toString()
		{
			return "NORMAL_NODE";
		}

	}

	public static class DownPointingNode extends NodeState
	{
		private static final long serialVersionUID = 5669739374747870906L;
		public static int STATE_ID = 1;

		public int getStateId()
		{
			return STATE_ID;
		}

		private static DownPointingNode singleton = null;

		private DownPointingNode()
		{
		}

		public Node findInsertionPoint(Node node)
		{
			return node.findSmallestChildlessNeighbor();
		}

		public Node findEndCapNode(Node node)
		{
			Node largestDownPointer = null;
			int largestDownPointerWebId = -42;
			for (Node n : node.getConnections().getDownPointers())
				if (n.getWebIdValue() > largestDownPointerWebId)
				{
					largestDownPointerWebId = n.getWebIdValue();
					largestDownPointer = n;
				}
			return largestDownPointer.findEndCapNode();
		}

		public static DownPointingNode getSingleton()
		{
			if (singleton == null)
				singleton = new DownPointingNode();
			return singleton;
		}

		public String toString()
		{
			return "DOWN_POINTING_NODE";
		}

	}

	public static class EndCapNode extends NodeState
	{
		private static final long serialVersionUID = 7794708191531969123L;
		public static int STATE_ID = 2;

		public int getStateId()
		{
			return STATE_ID;
		}

		private static EndCapNode singleton = null;

		private EndCapNode()
		{
		}

		public Node findEndCapNode(Node node)
		{
			return node;
		}

		public Node findInsertionPoint(Node node)
		{
			if (node.getFold().webId.getValue() == 0)
				return node.getFold();

			else
				return node.findSmallestChildlessNeighbor();
		}

		public static EndCapNode getSingleton()
		{
			if (singleton == null)
				singleton = new EndCapNode();
			return singleton;
		}

		public String toString()
		{
			return "END_CAP_NODE";
		}

	}

	// //////////////////////////////////////////////////// Template Pattern
	private class PointDownToParent extends Operation
	{
		public PointDownToParent(Node container)
		{
			super(container);
		}

		public Collection<Node> getCollection()
		{
			return container.getNeighbors();
		}

		public void appropriateOperation(Node neighbor, Node parent)
		{
			if (neighbor != parent)
			{
				neighbor.addDownPointer(parent);
				parent.addUpPointer(neighbor);
			}
		}
	}

	private class RemoveSelfFromNeighbors extends Operation
	{

		public RemoveSelfFromNeighbors(Node container)
		{
			super(container);
		}

		public Collection<Node> getCollection()
		{
			return container.getNeighbors();
		}

		public void appropriateOperation(Node neighbor, Node toBeRemoved)
		{
			neighbor.removeNeighbor(toBeRemoved);
		}
	}

	private class RemoveSelfAsUpPointer extends Operation
	{

		public RemoveSelfAsUpPointer(Node container)
		{
			super(container);
		}

		@Override
		public Collection<Node> getCollection()
		{
			return container.getDownPointers();
		}

		@Override
		public void appropriateOperation(Node down, Node target)
		{
			down.removeUpPointer(target);
		}

	}
}